#!/usr/bin/python

###########################################################################
# bughauz 0.1: a free, multiplayer bughouse program, still in development #
# Copyright (C) 2009 Martijn van Schaardenburg                            #
#                                                                         #
# This file is part of bughauz.                                           #
#                                                                         #
# bughauz is free software: you can redistribute it and/or modify         #
# it under the terms of the GNU General Public License as published by    #
# the Free Software Foundation, either version 3 of the License, or       #
# (at your option) any later version.                                     #
#                                                                         #
# This program is distributed in the hope that it will be useful,         #
# but WITHOUT ANY WARRANTY; without even the implied warranty of          #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           #
# GNU General Public License for more details.                            #
#                                                                         #
# You should have received a copy of the GNU General Public License       #
# along with this program.  If not, see <http://www.gnu.org/licenses/>.   #
#                                                                         #
# contact: mvanschaarde@hmc.edu or                                        #
# Martijn van Schaardenburg                                               #
# c/o Harvey Mudd College                                                 #
# 340 E. Foothill Blvd,                                                   #
# Claremont, Ca 91711                                                     #
#                                                                         #
# The images included as part of this release are covered under the LGPL  #
# and are Copyright (C) Maurizio Monge.  For more information, contact    #
# maurizio.monge@gmail.com.                                               #
###########################################################################

import sys, os, pygame, socket, random, string, pickle, select, time

WHITE = 0
BLACK = 1

BUGHOUSE_PORTNUM = 31255
MESSAGE_SIZE = 4096
PASSCODE = '12345'

PRINTABLE_CHARACTERS = ' 1234567890abcdefghijklmnopqrstuvwxyz'

BORDER = 40

SMALL_SQUARE_WIDTH = 30
LARGE_SQUARE_WIDTH = 48

PLAYER_HEIGHT = 50
DROPS_HEIGHT = LARGE_SQUARE_WIDTH

COMMAND_PANEL_HEIGHT = 50
LABELS_HEIGHT = 24

DARK_SQUARE_COLOR = (104, 68, 22)
LIGHT_SQUARE_COLOR = (208, 183, 150)
BLACK_COLOR = (0, 0, 0)
WHITE_COLOR = (255, 255, 255)
OUTLINE_COLOR = (130, 30, 20)

COMMAND_PANEL_COLOR = BLACK_COLOR
COMMAND_PANEL_TEXT_COLOR = (200, 255, 200)
COMMAND_PANEL_BORDER_COLOR = (100, 100, 100)

TEXT_OFFSET = 8

MOVEMENT_DIRECTIONS = {'q': [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)],
                       'r': [(-1, 0), (1, 0), (0, -1), (0, 1)],
                       'b': [(-1, -1), (-1, 1), (1, -1), (1, 1)]}
                       
MOVEMENT_LOCATIONS = {'n': [(-2, -1), (-2, 1), (-1, -2), (-1, 2), (1, -2), (1, 2), (2, -1), (2, 1)],
                      'k': [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]}

# the 'server' global variable has been removed because it's causing all manner of trouble.
# if you can find out how to fix it, put it back in.

class Piece:
    def __init__(self, t):
        self.typ = t.lower()
        self.color = WHITE

        if t in string.ascii_uppercase:
            self.color = BLACK
        self.prom = False

        # only relevant for kings and non-promoted rooks        
        self.castleable = True
        
        # only relevant for pawns
        self.ep_vulnerable = False
            
    def __cmp__(self, other):
        if other == None:
            return -1
        
        c = cmp(self.typ, other.typ)
        if c != 0:
            return c
        
        return cmp(self.color, other.color)
    
    def __repr__(self):
        s = self.typ
        if self.color == BLACK:
            s = s.upper()
        if self.prom:
            s += 'p'
        return s
    
    def __nonzero__(self):
        return True
    
    def __hash__(self):
        return hash(self.typ + str(self.color))
        
    def promoted(self, t):
        if self.typ != 'p':
            raise ValueError, 'trying to promote non-pawn piece'
            
        p = Piece(t)
        p.color = self.color
        p.prom = True
        p.castleable = False
        return p
        
class Player:
    def __init__(self, n):
        self.name = n
        #self.name = random.choice(['jimbo', 'zombo', 'grendel', 'gulu'])
        
        # all of the below set by server.
        self.color = None
        self.ID = None
        self.team = None
        self.socket = None
        self.address = None
        
        # eew gross
        self.sock_ind = None
        
        # set by main thread after the server decides where people go
        self.game_ind = None
        self.h = None
        
    def __hash__(self):
        if self.h == None:
            # could just use self.ID here
            self. h = hash(self.name + str(self.ID) + repr(self.game_ind))
        return self.h
        
    def __repr__(self):
        return '<player ' + self.name + ', with ID ' + str(self.ID) + \
               ' and sock_ind ' + str(self.sock_ind) + '>'
    
    # wow this is a mess. Perhaps none of this is needed except the second and third blocks
    def __cmp__(self, other):
        # check what is standard for this
        if type(other) != type(self):
            return -1
        
        c = cmp(self.name, other.name)
        if c != 0:
            return c
        
        # replacing "and" with "or" might work
        if self.ID != None and other.ID != None:
            return cmp(self.ID, other.ID)
        
        c = cmp(self.address, other.address)
        if c != 0:
            return c
        
        return cmp(self.socket, other.socket)

class Game:
    
    def __init__(self, p1, p2, start_timer):

        self.players = [p1, p2]
        self.opponents = {p1: p2, p2: p1}
        
        # lowercase is white.  a1 -> [0][0], h1->[0][8]
        # board[rank][file]
        b = [['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r'],
             ['p', 'p', 'p', 'p', 'p', 'p', 'p', 'p'],
             ['.', '.', '.', '.', '.', '.', '.', '.'],
             ['.', '.', '.', '.', '.', '.', '.', '.'],
             ['.', '.', '.', '.', '.', '.', '.', '.'],
             ['.', '.', '.', '.', '.', '.', '.', '.'],
             ['P', 'P', 'P', 'P', 'P', 'P', 'P', 'P'],
             ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R']]
             
        self.board = []
        for r in range(len(b)):
            self.board.append([])
            for f in range(len(b[r])):
                p = b[r][f]
                if p != '.':
                    self.board[-1].append(Piece(p))
                else:
                    self.board[-1].append(None)
        
        self.history = []
        
        self.drops = {p1: {Piece('p'): 0, Piece('n'): 0, Piece('b'): 0, Piece('r'): 0, Piece('q'): 0}, 
                      p2: {Piece('P'): 0, Piece('N'): 0, Piece('B'): 0, Piece('R'): 0, Piece('Q'): 0}}
        
        self.undo_requests = []
        
        t1 = [x for x in start_timer] + [None]
        t2 = [x for x in start_timer] + [None]
        
        self.timers = {p1: t1, p2: t2}
        self.to_move = p1
        self.winner = None
        self.started = False
        self.castlePossible = [True, True]
    
    def __repr__(self):
        return '<game object with board ' + repr(self.board) + 'and drops ' + repr(self.drops) + ' and players ' + repr(self.players) + '>'
    
    def undo(self):
        if len(self.history) > 0:
            self.board = self.history.pop()
            self.to_move = self.opponents[self.to_move]
            self.undo_requests = []
            self.winner = None
            
            # no time back for an undo, for now

    def updateBoardState(self, newBoard):
        self.history.append(self.board)
        self.board = newBoard
        self.undo_requests = []
        
        # timers here
        
    # TODO fix
    def startTimer(self, arg1 = None):
        return
    
    # does not allow en passant
    def tryMove(self, who, m):
        legal, castleside, ep = self.isLegal(who, m)
        
        if not legal:
            return (False, None, None)
        
        # TODO: add captured piece location, to account for en passant?
        updatedBoard, captured = self.make_move(who, m)
        
        if self.isPlayerInCheck(who):
            return (False, None, None)
        
        return (True, updatedBoard, captured)

    # does not check for self-checking (except in castling)
    def isLegal(self, who, m):
        if self.winner != None:
            return (False, None, False)
            
        if self.to_move != who:
            return (False, None, False)
        
        m = m.strip()
        
        s = m.lower().split()
        if s[0] == 'd':
            r, f = Game.rankFileFromChess(s[2])
            p = Piece(s[1])
            p.color = who.color
            
            return (self.isLegalDrop(who, p, r, f), None, False)
            
        movetype, fromstr, tostr = s
        
        fr, ff = Game.rankFileFromChess(fromstr)
        tr, tf = Game.rankFileFromChess(tostr)
        
        piece = self.board[fr][ff]
        if piece != None and piece.typ == 'k' and piece.castleable and tf in (2, 6):
            legal, side = self.isLegalCastle(who, fr, ff, tr, tf)
            return (legal, side, False)
            
        return (self.isLegalMove(who, fr, ff, tr, tf), None, False)
        
        
    def isLegalCastle(self, who, fr, ff, tr, tf):
        piece = self.board[fr][ff]
        if who.color == WHITE:
            rank = 0
        else:
            rank = 7
            
        if piece.typ != 'k' or piece.castleable != True:
            return (False, None)
        
        if fr != rank or tr != rank or ff != 4 or tf not in (2, 6):
            return (False, None)
        
        # cannot castle through check.
        for r in range(len(self.board)):
            for f in range(len(self.board[r])):
                opp = self.board[r][f]
                if opp != None and opp.color != who.color:
                    opponent_reachable = self.possibleSquares(opp, r, f) 
                    
                    # kingside
                    if tf == 6 and \
                       ((rank, 4) in opponent_reachable or \
                        (rank, 5) in opponent_reachable or \
                        (rank, 6) in opponent_reachable):
                         return (False, 'k')
                         
                     # queenside
                    elif tf == 2 and \
                       ((rank, 2) in opponent_reachable or \
                        (rank, 3) in opponent_reachable or \
                        (rank, 4) in opponent_reachable):
                         return (False, 'q')
                
        if tf == 6:
            if self.board[rank][7] == None or not self.board[rank][7].castleable:
                return (False, 'k')
            return (True, 'k')
        elif tf == 2:
            if self.board[rank][0] == None or not self.board[rank][0].castleable:
                return (False, 'q')
            return (True, 'q')
        
    def isLegalMove(self, who, fr, ff, tr, tf):
        piece = self.board[fr][ff]
        return piece != None and piece.color == who.color and \
               (tr, tf) in self.possibleSquares(piece, fr, ff)
    
    def possibleSquares(self, piece, r, f):
        ''' possibleSquares returns a list containing the squares that a given
        piece may move to.  Does not account for alleviating checks, en passant,
        or castling.  See isLegalMove for these.'''
        typ = piece.typ
        my_color = piece.color

        possible = []
        
        # queen, rook, bishop
        if typ in MOVEMENT_DIRECTIONS:
            for direction in MOVEMENT_DIRECTIONS[typ]:
                # symmetric, so 1, 0 or 0, 1 shouldn't matter.
                currr = r + direction[1]
                currf = f + direction[0]
                while 0 <= currr <= 7 and 0 <= currf <= 7 and self.board[currr][currf] == None:
                    possible.append((currr, currf))
                    
                    currr += direction[1]
                    currf += direction[0]
                
                # == None case redundant with above?
                if 0 <= currr <= 7 and 0 <= currf <= 7 and \
                     (self.board[currr][currf] == None or \
                      self.board[currr][currf].color != my_color):
                    possible.append((currr, currf))
        
        # king, knight
        elif typ in MOVEMENT_LOCATIONS:
            for location in MOVEMENT_LOCATIONS[typ]:
                currr = r + location[1]
                currf = f + location[0]
                if 0 <= currr <= 7 and 0 <= currf <= 7 and \
                     (self.board[currr][currf] == None or \
                      self.board[currr][currf].color != my_color):
                    possible.append((currr, currf))
        
        # hoo boy
        elif typ == 'p':
            if my_color == WHITE:
                orientation = 1
                start_rank = 1
            else:
                orientation = -1
                start_rank = 6
                
            if self.board[r + orientation][f] == None:
                possible.append((r + orientation, f))
                
                if r == start_rank and self.board[r + 2 * orientation][f] == None:
                    possible.append((r + 2 * orientation, f))
            
            if 0 <= f - 1 and self.board[r + orientation][f - 1] != None and \
                 self.board[r + orientation][f - 1].color == Game.otherColor(my_color):
                possible.append((r + orientation, f - 1))
            if 7 >= f + 1 and self.board[r + orientation][f + 1] != None and \
                 self.board[r + orientation][f + 1].color == Game.otherColor(my_color):
                possible.append((r + orientation, f + 1))
                
        return possible
    
    def isPlayerInCheck(self, who):
        opp_color = Game.otherColor(who.color)
        
        for r in range(8):
            for f in range(8):
                piece = self.board[r][f]
                if piece != None and piece.color == opp_color:
                    opp_reachable = self.possibleSquares(piece, r, f)
                    for (tr, tf) in opp_reachable:
                        to_piece = self.board[tr][tf]
                        if to_piece != None and \
                           to_piece.typ == 'k' and \
                           to_piece.color == who.color:
                            return True
        return False
    
    def isLegalDrop(self, who, piece, r, f):
        if self.board[r][f] != None:
            return False
        
        if piece.typ == 'p' and (r == 0 or r == 7):
            return False
        
        if self.drops[who][piece] == 0:
            return False
        elif self.drops[who][piece] < 0:
            raise ValueError
        
        # to do later:
        # must also do drop-check checking here if wanting to be good American
        # must check to be sure that drop alleviates all checks on king 
        return True
    
    # assumes that move is actually legal
    def make_move(self, who, move_str, move_time = None):
        chunks = move_str.split()
        
        # != None means the server is syncing the game timer
        #if move_time != None:
        #    self.timers[who.color][1] = 
            
        # quite wasteful, but can't just store move - need captured info and
        # way to roll back a move 
        
        board_copy = [[piece for piece in row] for row in self.board]
        
        if chunks[0] == 'd':
            r, f = Game.rankFileFromChess(chunks[2])
            p = Piece(chunks[1])
            p.color = who.color
            return self.make_drop(who, p, r, f, board_copy)

        else: # == 'm'
            r, f = Game.rankFileFromChess(chunks[1])
            r2, f2 = Game.rankFileFromChess(chunks[2])
            
            return self.make_move_move(who, r, f, r2, f2, board_copy)
    
    def make_move_move(self, who, fr, ff, tr, tf, board_copy,
                       castle = None, en_passant = False):
        #move_time = time.time()
        #if self.timers[who][2] == None:
        #    self.timers[who][2] = move_time
        #    self.timers[who][self.opponents[who]][2] = move_time
        
        # TODO make "castle" a useful argument
        if self.board[fr][ff].typ == 'k' and ff == 4 and tf in (2, 6):
            if tf == 6:
                castle = 'k'
            else:
                castle = 'q'
            return self.make_castle(who, castle, board_copy)
        
        captured = board_copy[tr][tf]
        
        # capture king win condition
        if captured != None and captured.typ == 'k':
            self.winner = who
            captured = None
            
        board_copy[tr][tf] = self.board[fr][ff]
        
        # for now, only promotion to queen
        # consider promotion command entered into command box
        
        piece = board_copy[tr][tf]
        if piece.typ == 'p' and abs(fr - tr) == 2:
            piece.ep_vulnerable = True
        
        if piece.typ == 'p' and (tr == 0 or tr == 7):
            # 'q' acceptable only because promoted sets color.
            board_copy[tr][tf] = piece.promoted('q')
        
        board_copy[fr][ff] = None
        self.to_move = self.opponents[who]
        
        return (board_copy, captured)
    
    def make_drop(self, who, piece, r, f, board_copy):
        self.drops[who][piece] -= 1
        board_copy[r][f] = piece 
        self.to_move = self.opponents[who]

        return (board_copy, None)
    
    def make_castle(self, who, side, board_copy):
        print 'making castle with side', side
        if who.color == WHITE:
            rank = 0
        else:
            rank = 7
            
        for p in self.board[rank]:
            if p != None:
                p.castleable = False
        
        # kingside castle
        if side == 'k':
            board_copy[rank][6] = board_copy[rank][4]
            board_copy[rank][4] = None
            board_copy[rank][5] = board_copy[rank][7]
            board_copy[rank][7] = None
        
        # queenside castle
        elif side == 'q':
            board_copy[rank][2] = board_copy[rank][4]
            board_copy[rank][4] = None
            board_copy[rank][3] = board_copy[rank][0]
            board_copy[rank][0] = None
                        
        # TODO: reorganize so that this code is not duplicated in make_move
        self.to_move = self.opponents[who]
        return (board_copy, None)
    
    @staticmethod
    def otherColor(color):
        return 1 - color
        
    # 'a8' -> (7, 0)
    @staticmethod
    def rankFileFromChess(s):
        return int(s[1]) - 1, ord(s[0]) - 97
    
    # (7, 0) -> 'a8'
    @staticmethod
    def chessFromRankFile(r, f):
        return chr(f + 97) + str(r + 1)
    
def setup():
    # contradicts config file paradigm
    serverIP = sys.argv[1].strip()
    
    # from config file, get: name, server IP, other stuff?
    name = 'player'
    try:
        f = open('info.conf', 'r')
        for line in f.readlines():
            exec(line.strip())
    except IOError:
        name = raw_input('Enter name: ').strip()
    
    me = Player(name)
    
    server_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_conn.connect((serverIP, BUGHOUSE_PORTNUM))
    server = [server_conn, '', []]
    
    print 'Sending personal information to server...'
    
    sendPlayerToServer(server, me)
    
    print 'Waiting for other players to connect...'
    server, msg = getCommunication(server, 'ug')
    games = pickle.loads(msg)
    
    print 'Matchup created.'
    
    server, msg = getCommunication(server, 'whoami')
    game_index, my_color = map(int, msg.split())
    
    print 'Additional player information retrieved.'
    
    if not me.name == games[game_index].players[my_color].name:
        raise ValueError, 'wrong player obtained: names do not match'
    
    # This update needs to include me.game_ind and me.color to be useful.
    # It should also have me.team.
    me = games[game_index].players[my_color]
    
    # stupid stupid
    me.game_ind = game_index
    
    return BughouseDisplay(games, me, server)
    
def clientMain():
    disp = setup()
    
    print 'Done initializing display. Let the games begin!'
    while True:
        disp.handleEvents()
        disp.displayAll()
        
        # needs win condition handling

def updateInbox(server):
    toRead, a, b = select.select([server[0]], [], [], 0)
    
    if len(toRead) == 1:
        server[1] += server[0].recv(MESSAGE_SIZE)
        server = processMbufs(server)
        
    return server
    
def getCommunication(server, comm_type):
    while True:
        toRead, a, b = select.select([server[0]], [], [], 0)
        
        if len(toRead) == 1:
            server[1] += server[0].recv(MESSAGE_SIZE)
            server = processMbufs(server)
            
        mbuf_list = server[2]
        for i in range(len(mbuf_list)):
            if mbuf_list[i].startswith(comm_type):
                
                # eew
                msg = removeTag(mbuf_list.pop(i))
                return [server[0], server[1], mbuf_list], msg
        
        time.sleep(0.1)
    
def processMbufs(tup):
    obj, mbuf, msg_list = tup
    while True:
        msg_chunks = mbuf.split()
        if len(msg_chunks) <= 2:
            break
        
        l = int(msg_chunks[1])
        
        msg_start = len(PASSCODE) + 2 + len(str(l))
        msg_end = msg_start + l
        msg_body = mbuf[msg_start: msg_end]
        
        if len(msg_body) < l:
            break
        
        msg_list.append(msg_body)
        mbuf = mbuf[msg_end:]
    
    return [obj, mbuf, msg_list]
    
def sendGlobalChatToServer(server, chat):
    raise TypeError, 'Not really a typeError.  Chat is not supported yet.'
    sendToServer(server, 'chat ' + chat)
    
def sendMoveToServer(server, mstr):
    # will send 'move m b e4 d3'
    sendToServer(server, 'move ' + mstr)
    
def sendDropToServer(server, mstr):
    # will send 'move d b a1'
    sendToServer('move d ' + mstr)
    
def sendUndoRequestToServer(server):
    sendToServer(server, 'undo')
    
def sendPlayerToServer(server, p):
    sendToServer(server, 'pinf ' + pickle.dumps(p))

def sendCommandToServer(server, com):
    com = com.strip().lower()
    if com.startswith('m ') or com.startswith('d '):
        sendToServer(server, 'move ' + com)
    elif com == 'u':
        sendToServer(server, 'undo')
        
def sendToServer(server, msg):
    server[0].sendall(PASSCODE + ' ' + str(len(msg)) + ' ' + msg)
    
def removeTag(s):
    ind = s.find(' ')
    return s[ind + 1:]

class GameRects:
    def __init__(self, left, board_width):
        
        heights = [PLAYER_HEIGHT, DROPS_HEIGHT, board_width, LABELS_HEIGHT, DROPS_HEIGHT, PLAYER_HEIGHT]
        tops = [BORDER]
        for h in heights:
            tops.append(tops[-1] + h)
            
        self.player_top_rect = pygame.Rect((left, tops[0], board_width, PLAYER_HEIGHT))
        
        pygame.Rect((left, tops[1], board_width, DROPS_HEIGHT))
        
        #separate rect into smaller rects
        # magic number: number of different droppable pieces
        drop_width = board_width / 5

        self.drops_top_rects = [pygame.Rect(left + i*drop_width,
                                            tops[1],
                                            drop_width,
                                            DROPS_HEIGHT) for i in range(5)]
        
        self.board_rect = pygame.Rect((left, tops[2], board_width, board_width))
        
        self.labels_bottom_rect = pygame.Rect((left, tops[3], board_width, LABELS_HEIGHT))
        

                                               
        self.player_bottom_rect = pygame.Rect((left, tops[4], board_width, DROPS_HEIGHT))
        
        self.drops_bottom_rects = [pygame.Rect(left + i*drop_width,
                                               tops[5],
                                               drop_width,
                                               DROPS_HEIGHT) for i in range(5)]

        self.labels_left_rect = pygame.Rect((left - LABELS_HEIGHT, tops[2], LABELS_HEIGHT, board_width))
        
class BughouseDisplay:
    
    def __init__(self, games, me, serv):
        self.me = me
        self.special_game_ind = me.game_ind
        self.server = serv

        self.games = games
        num_games = len(games)
        
        self.game_rects = []

        # these variables are a little ghetto
        left = BORDER
        width = 0
        
        pygame.init()
        
        for i in range(num_games):
            if i == self.special_game_ind:
                board_width = LARGE_SQUARE_WIDTH * 8
            else:
                board_width = SMALL_SQUARE_WIDTH * 8
            
            left += width
            width = board_width + BORDER
            
            self.game_rects.append(GameRects(left, board_width))
            
        total_width = (num_games + 1)*BORDER + \
                      (num_games - 1) * 8 * SMALL_SQUARE_WIDTH + \
                      8 * LARGE_SQUARE_WIDTH
        
        self.command_rect = pygame.Rect((BORDER, 
                                         2*BORDER + 2 * PLAYER_HEIGHT + 8 * LARGE_SQUARE_WIDTH + 2*DROPS_HEIGHT + LABELS_HEIGHT,
                                         total_width - 2 * BORDER,
                                         COMMAND_PANEL_HEIGHT))
        self.command_text = ''
        
        height = 3 * BORDER + 2 * DROPS_HEIGHT + 2 * PLAYER_HEIGHT + \
                 8 * LARGE_SQUARE_WIDTH + COMMAND_PANEL_HEIGHT + LABELS_HEIGHT
        
        self.screen = pygame.display.set_mode((total_width, height))
        pygame.display.set_caption('Bughouse')
        
        # Set up images
        img_base = {'p': 'white_pawn.png', 
                    'n': 'white_knight.png',
                    'b': 'white_bishop.png',
                    'r': 'white_rook.png',
                    'q': 'white_queen.png',
                    'k': 'white_king.png',
                    'P': 'black_pawn.png', 
                    'N': 'black_knight.png',
                    'B': 'black_bishop.png',
                    'R': 'black_rook.png',
                    'Q': 'black_queen.png',
                    'K': 'black_king.png'}

        self.images_large = {}
        self.images_small = {}
        
        small = (SMALL_SQUARE_WIDTH, SMALL_SQUARE_WIDTH)
        large = (LARGE_SQUARE_WIDTH, LARGE_SQUARE_WIDTH)
        
        for key in img_base:
            img = pygame.image.load(os.path.join('imgs', img_base[key]))
            
            self.images_small[Piece(key)] = pygame.transform.smoothscale(img, small)
            self.images_large[Piece(key)] = pygame.transform.smoothscale(img, large)
        
        self.default_font = pygame.font.Font(None, 24)
        self.selected = None, None
        
        pygame.display.flip()
        
    def handleEvents(self):
        self.server = updateInbox(self.server)
        while len(self.server[2]) > 0:
            self.processMsgToClient(self.server[2].pop(0))
        
        es = pygame.event.get()
        for e in es:
            if e.type == pygame.QUIT:
                sys.exit(0)
            elif e.type == pygame.KEYDOWN and e.key == pygame.K_ESCAPE:
                sys.exit(0)
            elif e.type == pygame.MOUSEBUTTONDOWN:
                left, top = pygame.mouse.get_pos()
                self.mouseClick(left, top, False)
            elif e.type == pygame.KEYDOWN:
                if e.key == pygame.K_RETURN:
                    sendCommandToServer(self.server, self.command_text)
                    self.command_text = ''
                elif e.key == pygame.K_BACKSPACE:
                    self.command_text = self.command_text[:-1]
                elif 0 <= e.key <= 127:
                    char = chr(e.key).lower()
                    if char in PRINTABLE_CHARACTERS:
                        self.command_text += char
            
            # TODO check for mouse click on drop spaces
            pygame.display.flip()
        pygame.event.pump()
        
    
    def processMsgToClient(self, msg):
        if msg[:2] == 'ug': # "update game state"
            self.games = pickle.loads(removeTag(msg))
        else:
            raise ValueError, 'invalid message from server:', msg
    
    def mouseClick(self, left, top, drop):
        my_rect = self.game_rects[self.special_game_ind].board_rect

        if my_rect.left <= left <= my_rect.left + my_rect.width and \
           my_rect.top  <= top  <= my_rect.top + my_rect.height:

            my_g = self.games[self.special_game_ind]
            
            ind_left = (left - my_rect.left) / (my_rect.width / 8)
            ind_top = (top - my_rect.top) / (my_rect.height / 8)
        
            old = self.selected
            
            self.deselect(old)
    
            color_on_bottom = (self.special_game_ind + self.me.team) % 2
            
            if old == (None, None):
                self.select((ind_left, ind_top))
            elif old != (ind_left, ind_top):

                if color_on_bottom == WHITE:
                    fr = 7 - old[1]
                    tr = 7 - ind_top
    
                    ff = old[0]
                    tf = ind_left
                    
                else:
                    fr = old[1]
                    tr = ind_top
    
                    ff = 7 - old[0]
                    tf = 7 - ind_left
                
                movestr = 'm ' + Game.chessFromRankFile(fr, ff) + \
                          ' ' + Game.chessFromRankFile(tr, tf)
                
                (legal, new_board, captured) = my_g.tryMove(self.me, movestr)
                
                if legal:
                    my_g.updateBoardState(new_board)
                    sendMoveToServer(self.server, movestr)
        
    # Consider merging select and deselect.

    def select(self, loc):
        left, top = loc
        if left == None and top == None:
            return
        
        self.selected = left, top
            
    def deselect(self, loc):
        left, top = loc
        if left == None and top == None:
            return
            
        self.selected = None, None

    def displayAll(self):
        for b in range(len(self.games)):
            self.displayGame(b)
        
        self.displayCommandPanel(self.default_font)
        
    def displayGame(self, ind):
        # dark magic here
        color_on_bottom = (ind + self.me.team) % 2
        
        if ind == self.special_game_ind:
            img_dict = self.images_large
        else:
            img_dict = self.images_small
        
        g = self.games[ind]
        bottom_p = g.players[color_on_bottom]
        top_p = g.opponents[bottom_p]
        
        bottom_d = g.drops[bottom_p]
        top_d = g.drops[top_p]
        
        g_rect = self.game_rects[ind]
        
        f = self.default_font
        
        self.displayPlayer(top_p, g_rect.player_top_rect, f)
        self.displayPlayer(bottom_p, g_rect.player_bottom_rect, f)
        
        self.displayDrops(top_d, g_rect.drops_top_rects, img_dict, f)
        self.displayDrops(bottom_d, g_rect.drops_bottom_rects, img_dict, f)
        
        self.displayLeftLabels(g_rect.labels_left_rect, color_on_bottom, f)
        self.displayBottomLabels(g_rect.labels_bottom_rect, color_on_bottom, f)
        self.displayBoard(g, g_rect.board_rect, img_dict, ind, color_on_bottom)
    
    def displayPlayer(self, p, rect, f):
        tstr = self.timingString(self.games[p.game_ind].timers[p])
        
        # magic number: width of player field
        text = f.render(p.name + (' '*(30-len(p.name))) + tstr, True, WHITE_COLOR)
        self.screen.blit(text, (rect.left, rect.top + TEXT_OFFSET))
        
    def timingString(self, timer):
        # magic numbers: number of seconds in a minute.  Also, the metric system.
        if timer == None:
            return 'Empty'
        return str(timer[0] / (60*1000)) + ':' + \
               ('0' + str((timer[0] % (60*1000)) / 1000))[-2:]
    
    def displayDrops(self, drops, drop_rects, img_dict, f):
        # TODO: fix all of this: from one rect to 5
        return

        d_width = float(rect.width)/10.0
        
        pygame.draw.rect(self.screen, BLACK_COLOR, rect)
    
        i = 0
        for piece in drops:
            num = f.render(str(drops[piece]), True, WHITE_COLOR)
            self.screen.blit(num, (rect.left + 2 * i * d_width + TEXT_OFFSET, rect.top + TEXT_OFFSET))
            self.screen.blit(img_dict[piece], (rect.left + (2 * i + 1) * d_width, rect.top))
            
            i += 1
            
    def displayLeftLabels(self, rect, color_on_bottom, f):
        l_height = rect.height / 8.0
        l_width = rect.width
        
        for rank in range(1, 9):
            if color_on_bottom == WHITE:
                l_text = str(9 - rank)
            else:
              l_text = str(rank)
              
            l_render = f.render(l_text, True, WHITE_COLOR)
            self.screen.blit(l_render, (rect.left + TEXT_OFFSET, rect.top + (rank - 1)*l_height + TEXT_OFFSET))
    
    def displayBottomLabels(self, rect, color_on_bottom, f):
        l_height = rect.height
        l_width = rect.width / 8.0

        for fil in range(1, 9):
            if color_on_bottom == WHITE:
                l_text = chr(fil + 96)
            else:
                l_text = chr(105 - fil)
            
            l_render = f.render(l_text, True, WHITE_COLOR)
            self.screen.blit(l_render, (rect.left + (fil - 1)*l_width + TEXT_OFFSET, rect.top + TEXT_OFFSET))
    
    # could pass only game.board instead of game - doesn't matter
    def displayBoard(self, game, board_rect, img_dict, ind, color_on_bottom):
        board = game.board
        
        sq_width  = board_rect.width / 8
        sq_height = board_rect.height / 8
        
        for row in range(8):
            for col in range(8):
                
                if color_on_bottom == WHITE:
                    display_row = 7 - row
                    display_col = col
                else:
                    display_row = row
                    display_col = 7 - col
                
                top = board_rect.top + sq_height * display_row
                left = board_rect.left + sq_width * display_col
                
                sq_rect = pygame.Rect((left, top, sq_width, sq_height))
                if (display_row + display_col) % 2 == 0:
                    sq_color = LIGHT_SQUARE_COLOR                 
                else:
                    sq_color = DARK_SQUARE_COLOR

                if display_row == self.selected[1] and \
                   display_col == self.selected[0] and \
                   ind == self.special_game_ind:
                    sq_outline_color = OUTLINE_COLOR
                else:
                    sq_outline_color = BLACK_COLOR
                    
                self.drawSquare(sq_color, sq_outline_color, sq_rect)
                
                piece = board[row][col]
                if piece in img_dict:  
                    self.screen.blit(img_dict[piece], (left, top))
                    
    def drawSquare(self, sq_color, sq_outline_color, rect):
        pygame.draw.rect(self.screen, sq_color, rect)
        pygame.draw.rect(self.screen, sq_outline_color, rect, 1)

    def displayCommandPanel(self, f):
        pygame.draw.rect(self.screen, COMMAND_PANEL_COLOR, self.command_rect)
        pygame.draw.rect(self.screen, COMMAND_PANEL_BORDER_COLOR, self.command_rect, 2)
        
        command_render = f.render(self.command_text, True, COMMAND_PANEL_TEXT_COLOR)
        self.screen.blit(command_render, (self.command_rect.left + TEXT_OFFSET, self.command_rect.top + 10))
        
if __name__ == '__main__':
    clientMain()
